
// Copyright (C) 2009-2011 Luca Piccioni
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//   
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Xml;

using Derm.OpenGL;
using Derm.Raster;

namespace Derm.Render
{
	/// <summary>
	/// Two dimensional texture.
	/// </summary>
	/// <remarks>
	/// <para>
	/// 
	/// </para>
	/// <para>
	/// 
	/// </para>
	/// </remarks>
	[System.Diagnostics.DebuggerDisplay("Texture2d [ Pixel:{mPixelFormat} Width:{Width} Height:{Height} ]")]
	public class Texture2d : Texture
	{
		#region Constructors

		/// <summary>
		/// Construct an undefined Texture2d.
		/// </summary>
		/// <remarks>
		/// <para>
		/// It creates Texture object which has not defined extents, internal format and textels. To define this texture, call one
		/// of <see cref="Create"/> methods (except <see cref="Create(RenderContext)"/>).
		/// </para>
		/// </remarks>
		public Texture2d()
		{

		}

		/// <summary>
		/// Create Texture2d data, defining only the extents and the internal format.
		/// </summary>
		/// <param name="width">
		/// A <see cref="UInt32"/> that specifies the texture width.
		/// </param>
		/// <param name="height">
		/// A <see cref="UInt32"/> that specifies the texture height.
		/// </param>
		/// <param name="format">
		/// A <see cref="Pixel.Type"/> determining the texture internal format.
		/// </param>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is zero.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> equals to <see cref="Pixel.Type.None"/>.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is greater than
		/// the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by the current context and <paramref name="width"/>
		/// or <paramref name="height"/> is not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> is not a supported internal format.
		/// </exception>
		public Texture2d(uint width, uint height, Pixel.Type format) : this(null, width, height, format)
		{
			
		}

		/// <summary>
		/// Create Texture2d data, defining only the extents and the internal format.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this Texture. If it null, the current context
		/// will be used.
		/// </param>
		/// <param name="width">
		/// A <see cref="UInt32"/> that specifies the texture width.
		/// </param>
		/// <param name="height">
		/// A <see cref="UInt32"/> that specifies the texture height.
		/// </param>
		/// <param name="format">
		/// A <see cref="Pixel.Type"/> determining the texture internal format.
		/// </param>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is zero.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> equals to <see cref="Pixel.Type.None"/>.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is greater than
		/// the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by <paramref name="ctx"/> (or by the current context is <paramref name="ctx"/> is
		/// null), and <paramref name="width"/> or <paramref name="height"/> is not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> is not a supported internal format.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception throw if <paramref name="lazy"/> is true and no <see cref="TextureService"/> is running on the <paramref name="ctx"/>
		/// object name space.
		/// </exception>
		public Texture2d(RenderContext ctx, uint width, uint height, Pixel.Type format)
		{
			Create(ctx, width, height, format);
		}
		
		#endregion

		#region Texture Data Storage

		/// <summary>
		/// Get the base format for the specified texture pixel type.
		/// </summary>
		/// <param name="pixelType">
		/// A <see cref="Pixel.Type"/> that specify the internal format of this Texture.
		/// </param>
		/// <returns>
		/// It returns a integer value corresponding to the OpenGL constant for the base format
		/// of <paramref name="pixelType"/>.
		/// </returns>
		protected virtual int GetTextureBaseFormat(Pixel.Type pixelType)
		{
			return (Pixel.GetGlFormat(pixelType));
		}

		/// <summary>
		/// Get the default base format.
		/// </summary>
		/// <returns>
		/// It returns a integer value corresponding to the OpenGL constant for the default base format
		/// of this texture.
		/// </returns>
		protected virtual int GetDefaultTextureBaseFormat()
		{
			return (Gl.RGBA);
		}

		/// <summary>
		/// Determine whether a pixel type is legal for this Texture instance.
		/// </summary>
		/// <param name="pixelType">
		/// A <see cref="Pixel.Type"/> that specify the internal format of this Texture.
		/// </param>
		/// <returns>
		/// It returns a boolean value indicating whether <paramref name="pixelType"/> is a legal value
		/// for this Texture instance.
		/// </returns>
		protected virtual bool IsLegalInternalFormat(Pixel.Type pixelType)
		{
			return (true);
		}

		/// <summary>
		/// Texture data, including mipmaps.
		/// </summary>
		private List<TextureData> mTextureData = null;

		#endregion

		#region Texture Creation

		#region Empty Texture

		/// <summary>
		/// Create a Texture2d, defining the texture extents and the internal format.
		/// </summary>
		/// <param name="width">
		/// A <see cref="UInt32"/> that specifies the texture width.
		/// </param>
		/// <param name="height">
		/// A <see cref="UInt32"/> that specifies the texture height.
		/// </param>
		/// <param name="format">
		/// A <see cref="Pixel.Type"/> determining the texture internal format.
		/// </param>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is zero.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> equals to <see cref="Pixel.Type.None"/>.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is greater than
		/// the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by current context, and <paramref name="width"/> or <paramref name="height"/>
		/// is not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> is not a supported internal format.
		/// </exception>
		public void Create(uint width, uint height, Pixel.Type format)
		{
			Create(null, width, height, format);
		}

		/// <summary>
		/// Create Texture2d data, defining only the extents and the internal format.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this Texture. If it null, the current context
		/// will be used.
		/// </param>
		/// <param name="width">
		/// A <see cref="UInt32"/> that specifies the texture width.
		/// </param>
		/// <param name="height">
		/// A <see cref="UInt32"/> that specifies the texture height.
		/// </param>
		/// <param name="format">
		/// A <see cref="Pixel.Type"/> determining the texture internal format.
		/// </param>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is zero.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> equals to <see cref="Pixel.Type.None"/>.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is greater than
		/// the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by <paramref name="ctx"/>, and <paramref name="width"/> or <paramref name="height"/>
		/// is not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> is not a supported internal format.
		/// </exception>
		public void Create(RenderContext ctx, uint width, uint height, Pixel.Type format)
		{
			// Define empty texture
			Define(ref ctx, width, height, format);
			// Create texture (with not pixel data associated)
			Create(ctx);
		}

		/// <summary>
		/// Define Texture2d data, defining only the extents and the internal format.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this Texture. If it null, the current context
		/// will be used.
		/// </param>
		/// <param name="width">
		/// A <see cref="UInt32"/> that specifies the texture width.
		/// </param>
		/// <param name="height">
		/// A <see cref="UInt32"/> that specifies the texture height.
		/// </param>
		/// <param name="format">
		/// A <see cref="Pixel.Type"/> determining the texture internal format.
		/// </param>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is zero.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> equals to <see cref="Pixel.Type.None"/>.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="width"/> or <paramref name="height"/> is greater than
		/// the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by <paramref name="ctx"/> (or by the current context is <paramref name="ctx"/> is
		/// null), and <paramref name="width"/> or <paramref name="height"/> is not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="format"/> is not a supported internal format.
		/// </exception>
		private void Define(ref RenderContext ctx, uint width, uint height, Pixel.Type format)
		{
			if (width == 0)
				throw new ArgumentException("zero width", "width");
			if (height == 0)
				throw new ArgumentException("zero height", "height");
			if (format == Pixel.Type.None)
				throw new ArgumentException("invalid pixel format", "format");

			// No pixel data (texture is defined, but no data is uploaded)
			mTextureData = null;

			// Define texture extents
			mWidth = width;
			mHeight = height;
			// Define texture internal format
			PixelFormat = format;

			// If not specified, get the current context
			if (ctx == null) {
				if ((ctx = RenderContext.GetCurrentContext()) == null)
					throw new InvalidOperationException("no current context");
			}

			// Check context compatibility
			CheckCapabilities(ctx, Width, Height, Depth, PixelFormat);
		}

		#endregion

		#region Image Texture

		/// <summary>
		/// Create Texture2d data from a Image instance.
		/// </summary>
		/// <param name="image">
		/// An <see cref="Derm.Raster.Image"/> holding the texture data.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception throw if <paramref name="image"/> is null.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> pixel data is not allocated (i.e. image not defined).
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> width or height are greater than the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by current context, and <paramref name="image"/> width or height are
		/// not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> format (<see cref="Image.PixelFormat"/> is not a supported internal format.
		/// </exception>
		public void Create(Image image)
		{
			Create(null, image);
		}

		/// <summary>
		/// Create Texture2d from a Image instance.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this Texture. If it null, the current context
		/// will be used.
		/// </param>
		/// <param name="image">
		/// An <see cref="Derm.Raster.Image"/> holding the texture data.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception throw if <paramref name="image"/> is null.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> pixel data is not allocated (i.e. image not defined).
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> width or height are greater than the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by <paramref name="ctx"/> (or the current context if <paramref name="ctx"/> is
		/// null), and <paramref name="image"/> width or height are not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> format (<see cref="Image.PixelFormat"/> is not a supported internal format.
		/// </exception>
		public void Create(RenderContext ctx, Image image)
		{
			// Define image texture
			Define(ref ctx, image);
			// Define texture
			Create(ctx);
		}

		/// <summary>
		/// Define Texture2d data from a Image instance.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this Texture. If it null, the current context
		/// will be used.
		/// </param>
		/// <param name="image">
		/// An <see cref="Derm.Raster.Image"/> holding the texture data.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// Exception throw if <paramref name="image"/> is null.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> pixel data is not allocated (i.e. image not defined).
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception thrown if <paramref name="ctx"/> is null and no context is current to the calling thread.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> width or height are greater than the maximum allowed for 2D textures.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if NPOT texture are not supported by <paramref name="ctx"/> (or the current context if <paramref name="ctx"/> is
		/// null), and <paramref name="image"/> width or height are not a power-of-two value.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception thrown if <paramref name="image"/> format (<see cref="Image.PixelFormat"/> is not a supported internal format.
		/// </exception>
		private void Define(ref RenderContext ctx, Image image)
		{
			if (image == null)
				throw new ArgumentNullException("image");
			if (image.GetPixelData() == null)
				throw new ArgumentException("no pixel data", "image");

			// Reference pixel data (texture contents, included mipmaps)
			mTextureData = new List<TextureData>();

			// The first layer
			TextureData layerBase = new TextureData();
			DefineTextureData(image, layerBase);
			mTextureData.Add(layerBase);

			// Define texture extents (match with 'image')
			mWidth = image.Width;
			mHeight = image.Height;
			// Define texture internal format (match with 'image')
			PixelFormat = image.PixelFormat;

			// If not specified, get the current context
			if (ctx == null) {
				if ((ctx = RenderContext.GetCurrentContext()) == null)
					throw new InvalidOperationException("no current context");
			}

			// Check context compatibility
			CheckCapabilities(ctx, Width, Height, Depth, PixelFormat);
		}

		#endregion

		#endregion

		#region Texture Download

		/// <summary>
		/// Download Texture2D data to an Image instance.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for downloading texture data.
		/// </param>
		/// <param name="pType">
		/// A <see cref="Pixel.Type"/> determining the pixel format of the downloaded data.
		/// </param>
		/// <returns>
		/// </returns>
		public Image Get(RenderContext ctx, Pixel.Type pType)
		{
			return (Get(ctx, pType, TextureTarget)[0]);
		}

		#endregion

		#region Texture Overrides

		/// <summary>
		/// Release texture data.
		/// </summary>
		/// <remarks>
		/// <para>
		/// If the current OpenGL implementation  supports texture objects, it is possible to release host texture data,
		/// since it is managed by OpenGL. To avoid unnecessary data duplication, Texture2d implementation release
		/// the textel data, including all defined mipmaps.
		/// </para>
		/// </remarks>
		protected override void ReleaseData()
		{
			if (mTextureData != null)
				ReleaseData(mTextureData);
		}

		/// <summary>
		/// Texture width.
		/// </summary>
		public override uint Width { get { return (mWidth); } }

		/// <summary>
		/// Texture height.
		/// </summary>
		public override uint Height { get { return (mHeight); } }

		/// <summary>
		/// Texture depth.
		/// </summary>
		/// <remarks>
		/// Only Texture3d target has a depth. For every else texture target, it is set to 1.
		/// </remarks>
		public override uint Depth { get { return (1); } }

		/// <summary>
		/// Determine the derived Texture target.
		/// </summary>
		/// <remarks>
		/// In the case a this Texture is defined by multiple targets (i.e. cube map textures), this property
		/// shall returns always 0.
		/// </remarks>
		public override int TextureTarget { get { return (Gl.TEXTURE_2D); } }

		/// <summary>
		/// Uniform sampler type for managing this Texture.
		/// </summary>
		internal override int SamplerType { get { return (Gl.SAMPLER_2D); } }

		/// <summary>
		/// Texture width.
		/// </summary>
		private uint mWidth = 0;

		/// <summary>
		/// Texture height.
		/// </summary>
		private uint mHeight = 0;

		#endregion

		#region RenderResource Overrides

		/// <summary>
		/// Create this RenderResource.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this object.
		/// </param>
		/// <exception cref="ArgumentNullException">
		/// This exception is thrown if the parameter <paramref name="ctx"/> is null.
		/// </exception>
		/// <exception cref="ArgumentException">
		/// Exception throw if <see cref="Width"/> or <see cref="Height"/> equals to 0.
		/// </exception>
		/// <exception cref="InvalidOperationException">
		/// Exception throw if <see cref="Texture.PixelFormat"/> equals to <see cref="Derm.Pixel.Type.None"/>.
		/// </exception>
		public override void Create(RenderContext ctx)
		{
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (Width == 0)
				throw new InvalidOperationException("zero width");
			if (Height == 0)
				throw new InvalidOperationException("zero height");
			if (PixelFormat == Pixel.Type.None)
				throw new InvalidOperationException("invalid pixel format");

			// Note: this override is necessary to check invalid use of Create(RenderContext) method. Essentially
			// user code should not call directly this method, since he's not able to set minimum required attributes
			// to define a texture without a Create method call (except this one).

			// Base implementation
			base.Create(ctx);
		}

		/// <summary>
		/// Actually create this Texture2d resources.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for allocating resources.
		/// </param>
		protected override void CreateObject(RenderContext ctx)
		{
			// Bind this Texture
			Bind(ctx);
			// Upload texture data (common to all mipmaps)
			int internalFormat = Pixel.GetGlInternalFormat(PixelFormat, ctx);

			if ((mTextureData != null) && (mTextureData.Count > 0)) {
				for (int i = 0; i < mTextureData.Count; i++) {
					int type = Pixel.GetGlDataFormat(mTextureData[i].TextelFormat);
					int format = GetTextureBaseFormat(mTextureData[i].TextelFormat);

					// Set pixel transfer
					foreach (int alignment in new int[] { 8, 4, 2, 1 }) {
						if (mTextureData[0].Stride % alignment == 0) {
							Gl.PixelStore(Gl.UNPACK_ALIGNMENT, alignment);
							RenderException.DebugCheckErrors();
							break;
						}
					}

					// Upload texture contents
					Gl.TexImage2D(TextureTarget, mTextureData[i].MipmapLevel, internalFormat, (int)Width, (int)Height, 0, format, type, mTextureData[i].Data);
					RenderException.CheckErrors();
				}
			} else {
				int format = GetDefaultTextureBaseFormat();

				// Define texture contents
				Gl.TexImage2D(TextureTarget, 0, internalFormat, (int)Width, (int)Height, 0, format, /* Unused */ Gl.UNSIGNED_BYTE, null);
				RenderException.CheckErrors();
			}
			// Unbind this texture
			Unbind(ctx);
		}

		/// <summary>
		/// Read an element of the current XML serializable.
		/// </summary>
		/// <param name="elementName">
		/// A <see cref="System.String"/> that specify the element name.
		/// </param>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element.
		/// </param>
		public override void ReadElement(string elementName, XmlReader xmlReader)
		{
			if (elementName == null)
				throw new ArgumentNullException("elementName");
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			if        (elementName == XmlNameImagePath) {

			} else if (elementName == XmlNameWidth) {

			} else if (elementName == XmlNameHeight) {
				
			} else if (elementName == XmlNameInternalFormat) {

			} else if (elementName == XmlNameMinFilter) {

			} else if (elementName == XmlNameMagFilter) {

			} else if (elementName == XmlNameWrapS) {

			} else if (elementName == XmlNameWrapT) {

			} else
				base.ReadElement(elementName, xmlReader);		// Skip element children
		}

		/// <summary>
		/// Name of the element that specify the texture image path.
		/// </summary>
		private const string XmlNameImagePath = "ImagePath";

		/// <summary>
		/// Name of the element that specify the texture width.
		/// </summary>
		private const string XmlNameWidth = "Width";

		/// <summary>
		/// Name of the element that specify the texture height.
		/// </summary>
		private const string XmlNameHeight = "Height";

		/// <summary>
		/// Name of the element that specify the texture internal format.
		/// </summary>
		private const string XmlNameInternalFormat = "InternalFormat";

		/// <summary>
		/// Name of the element that specify the texture minification filter.
		/// </summary>
		private const string XmlNameMinFilter = "MinFilter";

		/// <summary>
		/// Name of the element that specify the texture magnification filter.
		/// </summary>
		private const string XmlNameMagFilter = "MagFilter";

		/// <summary>
		/// Name of the element that specify the S texture coordinate wrapping.
		/// </summary>
		private const string XmlNameWrapS = "WrapS";

		/// <summary>
		/// Name of the element that specify the T texture coordinate wrapping.
		/// </summary>
		private const string XmlNameWrapT = "WrapT";

		#endregion
	}
}
